进阶

您所在的位置:网站首页 leaflet canvas 进阶

进阶

#进阶| 来源: 网络整理| 查看: 265

前言

地图标注是一个较为复杂的课题,此中技术难点可单独成文,本文暂不过多叙述,单说图层如何实现某个字段的自动标注。同时适用图层以点图层为主,线和面如果以Path的中心点为标注点也可参考(如何找寻面状要素的标注点是一技术难点,因为很多情况会导致标注点在面状要素外部,比如环等等),另本文适用于图层要素较多,存在标注叠盖冲突的情况。

Leaflet的插件Leaflet.tooltip(原Leaflet.label),这个插件用来做标注,适合在图层要素较少的情况,这跟它的实现方式有关,它主要通过添加absolute的div来实现,所以当标注较多时,DOM元素也会随之增长,类似SVG。所以这个插件改名为Leaflet.tooltip是非常贴切的,Label是Label,tooltip是tooltip,popup是popup,不能混用,将tooltip设置为永久显示就变为标注只适用于特殊场合。实现效果隐去了高德底图,避免不必要的麻烦。图层数据量为10000-20000之间,运行流畅,处理了标注叠盖,如果不处理叠盖,将10000多个标注同时显示,运行也非常流畅,原因是用Canvas直接渲染,而非添加div或添加svg要素(5000个要素,就会让浏览器非常卡)。实现思路

1.首先最重要的,渲染方式选择Canvas的fillText,具体原因上面已提,添加div或添加svg要素的方式优点在于方便交互,但无法胜任标注数量较多的情况。

2.继承Leaflet的Canvas(Renderer),原因是,Canvas Renderer已完成Canvas Container的创建和销毁(_initContainer和_destroyContainer),实现了当视图漫游缩放时响应更新(_update、_redraw和_draw),这些逻辑大部分可重用,无需重载,即使重载,也需要调用基类的逻辑。故继承,可省却大量逻辑。

3.标注是附属在空间要素上的,所以标注点的位置无需在响应Canvas视图的漫游缩放再进行transform。这点非常重要,标注的逻辑放在要素更新之后,即此时要素的空间坐标已完成transform。

4.如需处理标注重叠,可以通过记录当前已绘制标注的数组,对即将要绘制的标注进行bounds的intersect判断。

参考,Leaflet的plugins页面给出的参考是Leaflet.LabelTextCollision。该插件可重点看下源码实现,大致思路是想通的,但它的实现加入了一些不必要的元素,另外它的扩展性也有待加强。本文将在此基础上进行优化。插件源码

https://gist.github.com/shengzheng1981/a01a8a7b3b85cc5bda6648571bc76745

本文需要对照源码进行阅读。

源码对应示例暂未上线,后期再补充,将会更新到Green GIS Manager托管示例。

源码解析

1.非核心代码,先可放一边,不影响整体逻辑。

a. 函数identify和事件响应_handleMouseHover的重载逻辑可以略去,该两段代码主要为了处理标注的悬停和点击交互。

b. _redraw与_draw, 无需重载,可直接看Leaflet.Canvas的源码,这里重写只用于调试。

c. removeLabel\clearLabel\redrawLabel\redraw, 这几个外部调用函数,本文暂不调用,这些函数服务于手动添加标注时对标注数据进行增删改的维护。

2.最核心代码,_text函数,标注的主要逻辑,对照代码,逐行说明。

先说明外部调用时,Label的数据结构:

labelRenderer.addLabel({ text: "Label", //标注内容 position: { //标注坐标 lat: e.latlng.lat, lng: e.latlng.lng }, font : { //标注字体 size: 12, color: "#000000", family: "YaHei", bold: 'bold' }, background: { //标注背景颜色及padding visible: false, color: "#000000", padding: 5 }, border: { //标注边框颜色及宽度 visible: false, color: "#000000", width: 5 }, zoom: { //标注的显示级别 min: 1, max: 18 } });

标注代码:

_text : function(layer) { if (layer.label && layer.label.text != undefined) { layer.label.drawn = false; //记录标注是否已绘制,后面重点讲解 if (!this._bounds || (layer._pxBounds && layer._pxBounds.intersects(this._bounds))) { //非常重要:只绘制落在当前视图的layer const zoom = this._map.getZoom(); const min = ((layer.label.zoom || {}).min || 1); const max = ((layer.label.zoom || {}).max || 18); if (zoom >= min && zoom label.drawn).map( label => L.bounds(L.point(label.point.x, label.point.y), L.point(label.width + label.point.x, label.height + label.point.y))) //上方注释与本句的最大区别,在于效率(运行效率!!!),当数据量较大时,filter过滤的效率无法接受!!!故通过_drawn数组来记录已绘制的label const object = this._drawn.map( (label:any) => L.bounds(L.point(label.point.x, label.point.y), L.point(label.width + label.point.x, label.height + label.point.y))) .find( item => item.intersects(bounds) ); if (object) { //当存在intersect的label时,跳出不绘制 this._ctx.restore(); return; } } //绘制边框 if ((layer.label.border || {}).visible) { this._ctx.lineJoin = 'bevel'; this._ctx.lineWidth = ((layer.label.border || {}).width || 5); this._ctx.strokeStyle = ((layer.label.border || {}).color || 'rgba(0,0,0,0)'); this._ctx.strokeRect(layer._point.x - ((layer.label.background || {}).padding || 5), layer._point.y - ((layer.label.background || {}).padding || 5) - 2, layer.label.width, layer.label.height); } //绘制背景 if ((layer.label.background || {}).visible) { this._ctx.fillStyle = ((layer.label.background || {}).color || 'rgba(0,0,0,0)'); this._ctx.fillRect(layer._point.x - ((layer.label.background || {}).padding || 5), layer._point.y - ((layer.label.background || {}).padding || 5) - 2, layer.label.width, layer.label.height); } this._ctx.textBaseline = 'top'; this._ctx.fillStyle = ((layer.label.font || {}).color || 'rgba(0,0,0,1)'); //绘制文本 this._ctx.fillText(layer.label.text, layer._point.x, layer._point.y); this._drawn.push(layer.label); //记录当前已绘制的标注 layer.label.drawn = true; this._ctx.restore(); } } } }

3.Leaflet视图重绘的调用堆栈

非常重要:当不进行重载时,Leaflet的调用堆栈如下(画图有点费时。。。)

第一条调用逻辑:canvas._update => fire('update') => canvas._updatePaths => canvas._redraw => canvas._draw => layer(circlemarker)._updatePath => this._updateCircle => (Label)this._text

第二条调用逻辑:canvas._update => fire('update') => canvas._updatePaths => layer._update => layer(circlemarker)._updatePath => this._updateCircle => (Label)this._text

为什么要在此说明?因为这个流程会在进行一次视图更新时进行两次标注逻辑,在小数据量时,可以忽略,当数据量较大时,就会产生较大影响!!

因此,代码对_updatePaths进行重载:

原逻辑(Canvas._updatePaths)

_updatePaths: function () { if (this._postponeUpdatePaths) { return; } var layer; this._redrawBounds = null; for (var id in this._layers) { layer = this._layers[id]; layer._update(); } this._redraw(); },

现逻辑,断开第二条调用逻辑

updatePaths: function () { if (this._postponeUpdatePaths) { return; } this._redrawBounds = null; this._redraw(); },

如何使用(Usage)

1.创建label pane

map.createPane("labelPane"); map.getPane('labelPane').style.zIndex = 450; //450,>默认的overlayPane的400, { //geojson features text: feature.properties[field], //标注内容,field 字段名称 position: { //标注坐标 lat: feature.geometry.coordinates[1], //点要素,面和线要素可求得中心点后添加,注意经纬度先后顺序 lng: feature.geometry.coordinates[0] }, font : { //标注字体 size: 12, color: "#000000", family: "YaHei", bold: 'bold' }, background: { //标注背景颜色及padding visible: false, color: "#000000", padding: 5 }, border: { //标注边框颜色及宽度 visible: false, color: "#000000", width: 5 }, zoom: { //标注的显示级别 min: 12, max: 18 } })

END



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3